"""
Extract client information from http user agent
The module does not try to detect all capabilities of browser in current form (it can easily be extended though).
Tries to
    * be fast
    * very easy to extend
    * reliable enough for practical purposes
    * assist python web apps to detect clients.
"""

__version__ = "1.7.8"


class DetectorsHub(dict):
    _known_types = ["os", "dist", "flavor", "browser"]

    def __init__(self, *args, **kw):
        dict.__init__(self, *args, **kw)
        for typ in self._known_types:
            self.setdefault(typ, [])
        self.registerDetectors()

    def register(self, detector):
        if detector.info_type not in self._known_types:
            self[detector.info_type] = [detector]
            self._known_types.insert(detector.order, detector.info_type)
        else:
            self[detector.info_type].append(detector)

    def __iter__(self):
        return iter(self._known_types)

    def registerDetectors(self):
        detectors = [
            v() for v in globals().values() if DetectorBase in getattr(v, "__mro__", [])
        ]
        for d in detectors:
            if d.can_register:
                self.register(d)


class DetectorBase(object):
    name = ""  # "to perform match in DetectorsHub object"
    info_type = "override me"
    result_key = "override me"
    order = 10  # 0 is highest
    look_for = "string to look for"
    skip_if_found = []  # strings if present stop processin
    can_register = False
    version_markers = [("/", " ")]
    allow_space_in_version = False
    _suggested_detectors = None
    platform = None
    bot = False

    def __init__(self):
        if not self.name:
            self.name = self.__class__.__name__
        self.can_register = self.__class__.__dict__.get("can_register", True)

    def detect(self, agent, result):
        # -> True/None
        word = self.checkWords(agent)
        if word:
            result[self.info_type] = dict(name=self.name)
            result["bot"] = self.bot
            version = self.getVersion(agent, word)
            if version:
                result[self.info_type]["version"] = version
            if self.platform:
                result["platform"] = {"name": self.platform, "version": version}
            return True

    def checkWords(self, agent):
        # -> True/None
        for w in self.skip_if_found:
            if w in agent:
                return False
        if isinstance(self.look_for, (tuple, list)):
            for word in self.look_for:
                if word in agent:
                    return word
        elif self.look_for in agent:
            return self.look_for

    def getVersion(self, agent, word):
        """
        => version string /None
        """
        version_markers = (
            self.version_markers
            if isinstance(self.version_markers[0], (list, tuple))
            else [self.version_markers]
        )
        version_part = agent.split(word, 1)[-1]
        for start, end in version_markers:
            if version_part.startswith(start) and end in version_part:
                version = version_part[1:]
                if end:  # end could be empty string
                    version = version.split(end)[0]
                if not self.allow_space_in_version:
                    version = version.split()[0]
                return version


class OS(DetectorBase):
    info_type = "os"
    can_register = False
    version_markers = [";", " "]
    allow_space_in_version = True
    platform = None


class Dist(DetectorBase):
    info_type = "dist"
    can_register = False
    platform = None


class Flavor(DetectorBase):
    info_type = "flavor"
    can_register = False
    platform = None


class Browser(DetectorBase):
    info_type = "browser"
    can_register = False


class Firefox(Browser):
    look_for = "Firefox"
    version_markers = [("/", "")]
    skip_if_found = ["SeaMonkey", "web/snippet"]


class SeaMonkey(Browser):
    look_for = "SeaMonkey"
    version_markers = [("/", "")]


class Konqueror(Browser):
    look_for = "Konqueror"
    version_markers = ["/", ";"]


class OperaMobile(Browser):
    look_for = "Opera Mobi"
    name = "Opera Mobile"

    def getVersion(self, agent, word):
        try:
            look_for = "Version"
            return agent.split(look_for)[1][1:].split(" ")[0]
        except IndexError:
            look_for = "Opera"
            return agent.split(look_for)[1][1:].split(" ")[0]


class Opera(Browser):
    look_for = "Opera"

    def getVersion(self, agent, word):
        try:
            look_for = "Version"
            return agent.split(look_for)[1][1:].split(" ")[0]
        except IndexError:
            look_for = "Opera"
            version = agent.split(look_for)[1][1:].split(" ")[0]
            return version.split("(")[0]


class OperaNew(Browser):
    """
    Opera after version 15
    """

    name = "Opera"
    look_for = "OPR"
    version_markers = [("/", "")]


class Netscape(Browser):
    look_for = "Netscape"
    version_markers = [("/", "")]


class Trident(Browser):
    look_for = "Trident"
    skip_if_found = ["MSIE", "Opera"]
    name = "Microsoft Internet Explorer"
    version_markers = ["/", ";"]
    trident_to_ie_versions = {
        "4.0": "8.0",
        "5.0": "9.0",
        "6.0": "10.0",
        "7.0": "11.0",
    }

    def getVersion(self, agent, word):
        return self.trident_to_ie_versions.get(
            super(Trident, self).getVersion(agent, word)
        )


class MSIE(Browser):
    look_for = "MSIE"
    skip_if_found = ["Opera"]
    name = "Microsoft Internet Explorer"
    version_markers = [" ", ";"]


class MSEdge(Browser):
    look_for = "Edge"
    skip_if_found = ["MSIE"]
    version_markers = ["/", ""]


class Galeon(Browser):
    look_for = "Galeon"


class WOSBrowser(Browser):
    look_for = "wOSBrowser"

    def getVersion(self, agent, word):
        pass


class Safari(Browser):
    look_for = "Safari"
    skip_if_found = ["Edge"]

    def checkWords(self, agent):
        unless_list = ["Chrome", "OmniWeb", "wOSBrowser", "Android"]
        if self.look_for in agent:
            for word in unless_list:
                if word in agent:
                    return False
            return self.look_for

    def getVersion(self, agent, word):
        if "Version/" in agent:
            return agent.split("Version/")[-1].split(" ")[0].strip()
        if "Safari/" in agent:
            return agent.split("Safari/")[-1].split(" ")[0].strip()
        else:
            return agent.split("Safari ")[-1].split(" ")[0].strip()  # Mobile Safari


class GoogleBot(Browser):
    # https://support.google.com/webmasters/answer/1061943
    look_for = [
        "Googlebot",
        "Googlebot-News",
        "Googlebot-Image",
        "Googlebot-Video",
        "Googlebot-Mobile",
        "Mediapartners-Google",
        "Mediapartners",
        "AdsBot-Google",
        "web/snippet",
    ]
    bot = True
    version_markers = [("/", ";"), ("/", " ")]


class GoogleFeedFetcher(Browser):
    look_for = "Feedfetcher-Google"
    bot = True

    def get_version(self, agent):
        pass


class RunscopeRadar(Browser):
    look_for = "runscope-radar"
    bot = True


class GoogleAppEngine(Browser):
    look_for = "AppEngine-Google"
    bot = True

    def get_version(self, agent):
        pass


class GoogleApps(Browser):
    look_for = "GoogleApps script"
    bot = True

    def get_version(self, agent):
        pass


class TwitterBot(Browser):
    look_for = "Twitterbot"
    bot = True


class MJ12Bot(Browser):
    look_for = "MJ12bot"
    bot = True


class YandexBot(Browser):
    # http://help.yandex.com/search/robots/agent.xml
    look_for = "Yandex"
    bot = True

    def getVersion(self, agent, word):
        return agent[agent.index("Yandex") :].split("/")[-1].split(")")[0].strip()


class BingBot(Browser):
    look_for = "bingbot"
    version_markers = ["/", ";"]
    bot = True


class BaiduBot(Browser):
    # http://help.baidu.com/question?prod_en=master&class=1&id=1000973
    look_for = [
        "Baiduspider",
        "Baiduspider-image",
        "Baiduspider-video",
        "Baiduspider-news",
        "Baiduspider-favo",
        "Baiduspider-cpro",
        "Baiduspider-ads",
    ]
    bot = True
    version_markers = ("/", ";")


class LinkedInBot(Browser):
    look_for = "LinkedInBot"
    bot = True


class ArchiveDotOrgBot(Browser):
    look_for = "archive.org_bot"
    bot = True


class YoudaoBot(Browser):
    look_for = "YoudaoBot"
    bot = True


class YoudaoBotImage(Browser):
    look_for = "YodaoBot-Image"
    bot = True


class RogerBot(Browser):
    look_for = "rogerbot"
    bot = True


class TweetmemeBot(Browser):
    look_for = "TweetmemeBot"
    bot = True


class WebshotBot(Browser):
    look_for = "WebshotBot"
    bot = True


class SensikaBot(Browser):
    look_for = "SensikaBot"
    bot = True


class YesupBot(Browser):
    look_for = "YesupBot"
    bot = True


class DotBot(Browser):
    look_for = "DotBot"
    bot = True


class PhantomJS(Browser):
    look_for = "Browser/Phantom"
    bot = True


class FacebookExternalHit(Browser):
    look_for = "facebookexternalhit"
    bot = True


class NokiaOvi(Browser):
    look_for = "S40OviBrowser"


class UCBrowser(Browser):
    look_for = "UCBrowser"


class BrowserNG(Browser):
    look_for = "BrowserNG"


class Dolfin(Browser):
    look_for = "Dolfin"


class NetFront(Browser):
    look_for = "NetFront"


class Jasmine(Browser):
    look_for = "Jasmine"


class Openwave(Browser):
    look_for = "Openwave"


class UPBrowser(Browser):
    look_for = "UP.Browser"


class OneBrowser(Browser):
    look_for = "OneBrowser"


class ObigoInternetBrowser(Browser):
    look_for = "ObigoInternetBrowser"


class TelecaBrowser(Browser):
    look_for = "TelecaBrowser"


class MAUI(Browser):
    look_for = "Browser/MAUI"

    def getVersion(self, agent, word):
        version = agent.split("Release/")[-1][:10]
        return version


class NintendoBrowser(Browser):
    look_for = "NintendoBrowser"


class AndroidBrowser(Browser):
    look_for = "Android"
    skip_if_found = ["Chrome", "Windows Phone"]

    # http://decadecity.net/blog/2013/11/21/android-browser-versions
    def getVersion(self, agent, word):
        pass


class Linux(OS):
    look_for = "Linux"
    platform = "Linux"

    def getVersion(self, agent, word):
        pass


class Blackberry(OS):
    look_for = "BlackBerry"
    platform = "BlackBerry"

    def getVersion(self, agent, word):
        pass


class BlackberryPlaybook(Dist):
    look_for = "PlayBook"
    platform = "BlackBerry"

    def getVersion(self, agent, word):
        pass


class WindowsPhone(OS):
    name = "Windows Phone"
    platform = "Windows"
    look_for = ["Windows Phone OS", "Windows Phone"]
    version_markers = [(" ", ";"), (" ", ")")]


class iOS(OS):
    look_for = ("iPhone", "iPad")
    skip_if_found = ["like iPhone"]


class iPhone(Dist):
    look_for = "iPhone"
    platform = "iOS"
    skip_if_found = ["like iPhone"]

    def getVersion(self, agent, word):
        version_end_chars = [" "]
        if not "iPhone OS" in agent:
            return None
        part = agent.split("iPhone OS")[-1].strip()
        for c in version_end_chars:
            if c in part:
                version = part.split(c)[0]
                return version.replace("_", ".")
        return None


class IPad(Dist):
    look_for = "iPad;"
    platform = "iOS"

    def getVersion(self, agent, word):
        version_end_chars = [" "]
        if not "CPU OS " in agent:
            return None
        part = agent.split("CPU OS ")[-1].strip()
        for c in version_end_chars:
            if c in part:
                version = part.split(c)[0]
                return version.replace("_", ".")
        return None


class Macintosh(OS):
    look_for = "Macintosh"

    def getVersion(self, agent, word):
        pass


class MacOS(Flavor):
    look_for = "Mac OS"
    platform = "Mac OS"
    skip_if_found = ["iPhone", "iPad"]

    def getVersion(self, agent, word):
        version_end_chars = [";", ")"]
        part = agent.split("Mac OS")[-1].strip()
        for c in version_end_chars:
            if c in part:
                version = part.split(c)[0]
                return version.replace("_", ".")
        return ""


class Windows(Dist):
    look_for = "Windows"
    platform = "Windows"


class Windows(OS):
    look_for = "Windows"
    platform = "Windows"
    skip_if_found = ["Windows Phone"]
    win_versions = {
        "NT 10.0": "10",
        "NT 6.3": "8.1",
        "NT 6.2": "8",
        "NT 6.1": "7",
        "NT 6.0": "Vista",
        "NT 5.2": "Server 2003 / XP x64",
        "NT 5.1": "XP",
        "NT 5.01": "2000 SP1",
        "NT 5.0": "2000",
        "98; Win 9x 4.90": "Me",
    }

    def getVersion(self, agent, word):
        v = agent.split("Windows")[-1].split(";")[0].strip()
        if ")" in v:
            v = v.split(")")[0]
        v = self.win_versions.get(v, v)
        return v


class Ubuntu(Dist):
    look_for = "Ubuntu"
    version_markers = ["/", " "]


class Debian(Dist):
    look_for = "Debian"
    version_markers = ["/", " "]


class Chrome(Browser):
    look_for = "Chrome"
    version_markers = ["/", " "]
    skip_if_found = ["OPR", "Edge"]

    def getVersion(self, agent, word):
        part = agent.split(word + self.version_markers[0])[-1]
        version = part.split(self.version_markers[1])[0]
        if "+" in version:
            version = part.split("+")[0]
        return version.strip()


class ChromeiOS(Browser):
    look_for = "CriOS"
    version_markers = ["/", " "]


class ChromeOS(OS):
    look_for = "CrOS"
    platform = " ChromeOS"
    version_markers = [" ", " "]

    def getVersion(self, agent, word):
        version_markers = self.version_markers
        if word + "+" in agent:
            version_markers = ["+", "+"]
        return (
            agent.split(word + version_markers[0])[-1]
            .split(version_markers[1])[1]
            .strip()[:-1]
        )


class Android(Dist):
    look_for = "Android"
    platform = "Android"
    skip_if_found = ["Windows Phone"]

    def getVersion(self, agent, word):
        return agent.split(word)[-1].split(";")[0].strip()


class WebOS(Dist):
    look_for = "hpwOS"

    def getVersion(self, agent, word):
        return agent.split("hpwOS/")[-1].split(";")[0].strip()


class NokiaS40(OS):
    look_for = "Series40"
    platform = "Nokia S40"

    def getVersion(self, agent, word):
        pass


class Symbian(OS):
    look_for = ["Symbian", "SymbianOS"]
    platform = "Symbian"


class PlayStation(OS):
    look_for = ["PlayStation", "PLAYSTATION"]
    platform = "PlayStation"
    version_markers = [" ", ")"]


class prefs:  # experimental
    os = dict(
        Linux=dict(dict(browser=[Firefox, Chrome], dist=[Ubuntu, Android])),
        BlackBerry=dict(dist=[BlackberryPlaybook]),
        Macintosh=dict(flavor=[MacOS]),
        Windows=dict(browser=[MSIE, Firefox]),
        ChromeOS=dict(browser=[Chrome]),
        Debian=dict(browser=[Firefox]),
    )
    dist = dict(
        Ubuntu=dict(browser=[Firefox]),
        Android=dict(browser=[Safari]),
        IPhone=dict(browser=[Safari]),
        IPad=dict(browser=[Safari]),
    )
    flavor = dict(MacOS=dict(browser=[Opera, Chrome, Firefox, MSIE]))


detectorshub = DetectorsHub()


def detect(agent, fill_none=False):
    """
    fill_none: if name/version is not detected respective key is still added to the result with value None
    """
    result = dict(platform=dict(name=None, version=None))
    _suggested_detectors = []

    for info_type in detectorshub:
        detectors = _suggested_detectors or detectorshub[info_type]
        for detector in detectors:
            try:
                detector.detect(agent, result)
            except Exception as _err:
                pass

    if fill_none:
        attrs_d = {"name": None, "version": None}
        for key in ("os", "browser"):
            if key not in result:
                result[key] = attrs_d
            else:
                for k, v in attrs_d.items():
                    result[k] = v

    return result


def simple_detect(agent):
    """
    -> (os, browser) # tuple of strings
    """
    result = detect(agent)
    os_list = []
    if "flavor" in result:
        os_list.append(result["flavor"]["name"])
    if "dist" in result:
        os_list.append(result["dist"]["name"])
    if "os" in result:
        os_list.append(result["os"]["name"])

    os = os_list and " ".join(os_list) or "Unknown OS"
    os_version = (
        os_list
        and (result.get("flavor") and result["flavor"].get("version"))
        or (result.get("dist") and result["dist"].get("version"))
        or (result.get("os") and result["os"].get("version"))
        or ""
    )
    browser = "browser" in result and result["browser"].get("name") or "Unknown Browser"
    browser_version = "browser" in result and result["browser"].get("version") or ""
    if browser_version:
        browser = " ".join((browser, browser_version))
    if os_version:
        os = " ".join((os, os_version))
    return os, browser


class mobilize(object):
    """
    Decorator for controller functions so they use different views for mobile devices.

    WARNING: If you update httpagentparser make sure to leave mobilize for
    backwards compatibility.
    """

    def __init__(self, func):
        self.func = func

    def __call__(self):
        from gluon import current

        user_agent = current.request.user_agent()
        if user_agent.is_mobile:
            items = current.response.view.split(".")
            items.insert(-1, "mobile")
            current.response.view = ".".join(items)
        return self.func()
